@twango/twiki 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/product-cache/action.yml +32 -0
- package/.github/actions/product-cache/index.mjs +450 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bun.lock +1701 -0
- package/cli/runtime.test.ts +420 -0
- package/cli/runtime.ts +385 -0
- package/cli/twiki.ts +305 -0
- package/package.json +138 -4
- package/site/_headers +7 -0
- package/site/assets/fonts/OFL.txt +93 -0
- package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/site/assets/social-background.svg +38 -0
- package/site/bun.lock +1701 -0
- package/site/database/index.ts +7 -0
- package/site/database/schema.ts +95 -0
- package/site/drizzle.config.ts +7 -0
- package/site/migrations/0001_auth.sql +14 -0
- package/site/migrations/0002_site_access.sql +5 -0
- package/site/migrations/0003_github_username.sql +1 -0
- package/site/migrations/meta/0002_snapshot.json +432 -0
- package/site/migrations/meta/0003_snapshot.json +439 -0
- package/site/migrations/meta/_journal.json +27 -0
- package/site/package.json +120 -0
- package/site/src/app.d.ts +14 -0
- package/site/src/app.html +11 -0
- package/site/src/hooks.server.ts +12 -0
- package/site/src/lib/analytics-config.json +1 -0
- package/site/src/lib/analytics.ts +40 -0
- package/site/src/lib/auth-config.json +3 -0
- package/site/src/lib/components/SEO.svelte +113 -0
- package/site/src/lib/components/doc/AccessLink.svelte +27 -0
- package/site/src/lib/components/doc/Backlinks.svelte +59 -0
- package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
- package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
- package/site/src/lib/components/doc/DocPager.svelte +91 -0
- package/site/src/lib/components/doc/DocShell.svelte +115 -0
- package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
- package/site/src/lib/components/doc/LockedContent.svelte +29 -0
- package/site/src/lib/components/doc/NavTree.svelte +158 -0
- package/site/src/lib/components/doc/NoteActions.svelte +139 -0
- package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
- package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
- package/site/src/lib/components/doc/Sidebar.svelte +351 -0
- package/site/src/lib/components/doc/Toc.svelte +339 -0
- package/site/src/lib/components/doc/enhancements.ts +146 -0
- package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
- package/site/src/lib/components/doc/image-preview.ts +114 -0
- package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
- package/site/src/lib/components/embeds/mermaid.ts +353 -0
- package/site/src/lib/components/files/FileCode.svelte +81 -0
- package/site/src/lib/components/files/FileExplorer.svelte +74 -0
- package/site/src/lib/components/files/FileHistory.svelte +170 -0
- package/site/src/lib/components/files/FileIcon.svelte +28 -0
- package/site/src/lib/components/files/FileRmd.svelte +209 -0
- package/site/src/lib/components/files/FileTabs.svelte +180 -0
- package/site/src/lib/components/files/FileTree.svelte +67 -0
- package/site/src/lib/components/files/blame-gutter.ts +45 -0
- package/site/src/lib/components/files/code-view.ts +98 -0
- package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
- package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
- package/site/src/lib/components/graph/GraphView.svelte +100 -0
- package/site/src/lib/components/graph/graph-data.ts +137 -0
- package/site/src/lib/components/graph/graph-model.ts +103 -0
- package/site/src/lib/components/graph/index.ts +3 -0
- package/site/src/lib/components/graph/render-graph.ts +626 -0
- package/site/src/lib/components/popover/index.ts +1 -0
- package/site/src/lib/components/popover/link-popovers.ts +233 -0
- package/site/src/lib/components/popover/popover.css +82 -0
- package/site/src/lib/components/search/SearchButton.svelte +57 -0
- package/site/src/lib/components/search/SearchPalette.svelte +189 -0
- package/site/src/lib/components/search/index.ts +3 -0
- package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
- package/site/src/lib/components/search/pagefind-query.ts +22 -0
- package/site/src/lib/components/search/pagefind-url.ts +12 -0
- package/site/src/lib/components/search/search-state.svelte.ts +22 -0
- package/site/src/lib/components/ui/IconButton.svelte +30 -0
- package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
- package/site/src/lib/components/ui/index.ts +1 -0
- package/site/src/lib/config.ts +9 -0
- package/site/src/lib/content-license.ts +7 -0
- package/site/src/lib/file-history.ts +37 -0
- package/site/src/lib/file-icons.ts +27 -0
- package/site/src/lib/file-tabs.ts +72 -0
- package/site/src/lib/files.ts +73 -0
- package/site/src/lib/github-user.ts +70 -0
- package/site/src/lib/metadata.ts +166 -0
- package/site/src/lib/notebook-nav.ts +63 -0
- package/site/src/lib/publication.ts +1 -0
- package/site/src/lib/rmd.ts +156 -0
- package/site/src/lib/server/access-admin.ts +33 -0
- package/site/src/lib/server/content-model.ts +253 -0
- package/site/src/lib/server/content-provider.ts +266 -0
- package/site/src/lib/server/content.ts +277 -0
- package/site/src/lib/server/runtime-content.ts +34 -0
- package/site/src/lib/site-config.json +9 -0
- package/site/src/lib/social-image.ts +13 -0
- package/site/src/lib/styles/article.css +607 -0
- package/site/src/lib/styles/foundation.css +138 -0
- package/site/src/lib/styles/print.css +115 -0
- package/site/src/lib/styles/shell.css +182 -0
- package/site/src/lib/text-exports.ts +5 -0
- package/site/src/lib/types.ts +140 -0
- package/site/src/lib/utils.ts +6 -0
- package/site/src/routes/+error.svelte +16 -0
- package/site/src/routes/+layout.server.ts +10 -0
- package/site/src/routes/+layout.svelte +72 -0
- package/site/src/routes/+layout.ts +3 -0
- package/site/src/routes/404/+page.svelte +13 -0
- package/site/src/routes/[...slug]/+page.server.ts +24 -0
- package/site/src/routes/[...slug]/+page.svelte +180 -0
- package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
- package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
- package/site/src/routes/admin/access/+page.server.ts +53 -0
- package/site/src/routes/admin/access/+page.svelte +118 -0
- package/site/src/routes/graph.json/+server.ts +7 -0
- package/site/src/routes/index.xml/+server.ts +7 -0
- package/site/src/routes/layout.css +10 -0
- package/site/src/routes/robots.txt/+server.ts +9 -0
- package/site/src/routes/sitemap.xml/+server.ts +7 -0
- package/site/src/routes/tags/+page.server.ts +5 -0
- package/site/src/routes/tags/+page.svelte +38 -0
- package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
- package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
- package/site/static/favicon.png +0 -0
- package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
- package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
- package/site/static/fonts/Virgil.woff2 +0 -0
- package/site/svelte.config.js +71 -0
- package/site/tooling/LICENSE-quartz +25 -0
- package/site/tooling/adapter.js +55 -0
- package/site/tooling/assets.ts +323 -0
- package/site/tooling/benchmark.ts +191 -0
- package/site/tooling/build.ts +407 -0
- package/site/tooling/cache.ts +133 -0
- package/site/tooling/check-diagrams.ts +164 -0
- package/site/tooling/check-links.ts +86 -0
- package/site/tooling/check-targets.ts +139 -0
- package/site/tooling/compiler.ts +1386 -0
- package/site/tooling/deploy.ts +112 -0
- package/site/tooling/fingerprint.ts +81 -0
- package/site/tooling/lib/app-version.js +92 -0
- package/site/tooling/lib/application-asset-urls.ts +27 -0
- package/site/tooling/lib/application-cache.ts +269 -0
- package/site/tooling/lib/asset-hash-cache.ts +116 -0
- package/site/tooling/lib/autotag.ts +99 -0
- package/site/tooling/lib/build-assembly.ts +148 -0
- package/site/tooling/lib/build-workspace.ts +386 -0
- package/site/tooling/lib/cache-selection.ts +70 -0
- package/site/tooling/lib/cloudflare-prewarm.js +40 -0
- package/site/tooling/lib/comments.ts +32 -0
- package/site/tooling/lib/content-cache.ts +244 -0
- package/site/tooling/lib/content-urls.ts +50 -0
- package/site/tooling/lib/course-cache.ts +298 -0
- package/site/tooling/lib/course-files.ts +281 -0
- package/site/tooling/lib/currency.ts +33 -0
- package/site/tooling/lib/deployment-config.ts +222 -0
- package/site/tooling/lib/deployment-manifest.ts +274 -0
- package/site/tooling/lib/deployment-prepare.ts +325 -0
- package/site/tooling/lib/deployment-upload.ts +293 -0
- package/site/tooling/lib/dev-git-state.ts +130 -0
- package/site/tooling/lib/dev-history.ts +86 -0
- package/site/tooling/lib/dev-social-images.ts +38 -0
- package/site/tooling/lib/dev-state.ts +107 -0
- package/site/tooling/lib/edition-builds.ts +184 -0
- package/site/tooling/lib/edition-paths.js +29 -0
- package/site/tooling/lib/empty-tag-route.js +23 -0
- package/site/tooling/lib/file-history.ts +201 -0
- package/site/tooling/lib/file-icons.ts +93 -0
- package/site/tooling/lib/file-policy.ts +38 -0
- package/site/tooling/lib/file-search.js +13 -0
- package/site/tooling/lib/git-revisions.ts +41 -0
- package/site/tooling/lib/history-pool.ts +74 -0
- package/site/tooling/lib/history-worker.ts +22 -0
- package/site/tooling/lib/knit-rmd.R +38 -0
- package/site/tooling/lib/lastmod.ts +201 -0
- package/site/tooling/lib/markdown-files.ts +51 -0
- package/site/tooling/lib/ofm.ts +634 -0
- package/site/tooling/lib/output.ts +60 -0
- package/site/tooling/lib/product-cache.ts +1006 -0
- package/site/tooling/lib/project-config.ts +288 -0
- package/site/tooling/lib/public-assets.ts +38 -0
- package/site/tooling/lib/public-links.ts +56 -0
- package/site/tooling/lib/publishing.ts +156 -0
- package/site/tooling/lib/r-runtime.ts +257 -0
- package/site/tooling/lib/rebuild-queue.ts +25 -0
- package/site/tooling/lib/rmd-preflight.ts +166 -0
- package/site/tooling/lib/rmd-preview-cache.ts +93 -0
- package/site/tooling/lib/rmd-previews.ts +129 -0
- package/site/tooling/lib/runtime-data-modules.ts +30 -0
- package/site/tooling/lib/runtime-packaging.ts +321 -0
- package/site/tooling/lib/search-cache.js +165 -0
- package/site/tooling/lib/search-inputs.js +129 -0
- package/site/tooling/lib/slug.ts +377 -0
- package/site/tooling/lib/social-images.ts +210 -0
- package/site/tooling/lib/text-exports.ts +51 -0
- package/site/tooling/lib/visit-elements.ts +17 -0
- package/site/tooling/live-test.ts +138 -0
- package/site/tooling/pipeline.ts +103 -0
- package/site/tooling/prepare.ts +3 -0
- package/site/tooling/publish.ts +28 -0
- package/site/tooling/r-cache.ts +45 -0
- package/site/tooling/r-render-profile.json +7 -0
- package/site/tooling/search.js +94 -0
- package/site/tooling/verify-deployment.ts +109 -0
- package/site/tooling/vite-content.ts +240 -0
- package/site/tooling/wrangler-paths.js +25 -0
- package/site/tooling/wrangler.ts +19 -0
- package/site/tsconfig.json +16 -0
- package/site/vite.config.ts +40 -0
- package/site/worker/access.ts +57 -0
- package/site/worker/analytics.ts +100 -0
- package/site/worker/auth-routes.ts +78 -0
- package/site/worker/auth.ts +78 -0
- package/site/worker/content-runtime.ts +226 -0
- package/site/worker/framing.ts +4 -0
- package/site/worker/gate.ts +147 -0
- package/site/worker/index.ts +9 -0
- package/site/worker/login.ts +62 -0
- package/site/worker/publication.ts +44 -0
- package/site/worker/runtime.ts +104 -0
- package/site/worker/tsconfig.json +13 -0
- package/site/wrangler.jsonc +37 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Encrypted build products
|
|
2
|
+
description: Restore or save independently verified application, course and search products
|
|
3
|
+
inputs:
|
|
4
|
+
mode:
|
|
5
|
+
description: restore or save
|
|
6
|
+
required: true
|
|
7
|
+
namespace:
|
|
8
|
+
description: Production caches, or isolated caches for trusted manual benchmarks
|
|
9
|
+
default: production
|
|
10
|
+
transport:
|
|
11
|
+
description: Individual group caches, or one bundle of independently encrypted groups
|
|
12
|
+
default: groups
|
|
13
|
+
outputs:
|
|
14
|
+
requested:
|
|
15
|
+
description: Product groups requested
|
|
16
|
+
hits:
|
|
17
|
+
description: Encrypted archive cache hits before product verification
|
|
18
|
+
misses:
|
|
19
|
+
description: Encrypted archive cache misses
|
|
20
|
+
restored:
|
|
21
|
+
description: Product groups verified and restored
|
|
22
|
+
rebuild_needed:
|
|
23
|
+
description: Requested groups unavailable for reuse
|
|
24
|
+
published:
|
|
25
|
+
description: Verified product archives published
|
|
26
|
+
skipped:
|
|
27
|
+
description: Save operations skipped or already cached
|
|
28
|
+
failed:
|
|
29
|
+
description: Optional cache operations that failed
|
|
30
|
+
runs:
|
|
31
|
+
using: node24
|
|
32
|
+
main: index.mjs
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
// A JavaScript action receives the Actions cache service credentials. Do not
|
|
18
|
+
// export those credentials into a workflow log or a repository artifact.
|
|
19
|
+
const site = process.env.TWIKI_SITE_DIR
|
|
20
|
+
? path.resolve(process.env.TWIKI_SITE_DIR)
|
|
21
|
+
: fileURLToPath(new URL("../../../site/", import.meta.url));
|
|
22
|
+
if (
|
|
23
|
+
process.env.TWIKI_SITE_DIR &&
|
|
24
|
+
(!path.isAbsolute(process.env.TWIKI_SITE_DIR) ||
|
|
25
|
+
!lstatSync(site).isDirectory() ||
|
|
26
|
+
realpathSync(site) !== site)
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Product cache requires a real absolute engine working directory",
|
|
30
|
+
);
|
|
31
|
+
const mode = process.env.INPUT_MODE;
|
|
32
|
+
if (!["restore", "save"].includes(mode))
|
|
33
|
+
throw new Error("Expected restore or save mode");
|
|
34
|
+
const namespace = process.env.INPUT_NAMESPACE || "production";
|
|
35
|
+
if (!["production", "benchmark"].includes(namespace))
|
|
36
|
+
throw new Error("Unsupported product cache namespace");
|
|
37
|
+
const transport = process.env.INPUT_TRANSPORT || "groups";
|
|
38
|
+
if (!["groups", "bundle"].includes(transport))
|
|
39
|
+
throw new Error("Unsupported product cache transport");
|
|
40
|
+
if (
|
|
41
|
+
transport === "bundle" &&
|
|
42
|
+
!/^[a-f0-9]{40}$/.test(process.env.GITHUB_SHA ?? "")
|
|
43
|
+
)
|
|
44
|
+
throw new Error("Bundle caches require an exact Git commit");
|
|
45
|
+
if (
|
|
46
|
+
namespace === "benchmark" &&
|
|
47
|
+
(process.env.GITHUB_REF !== "refs/heads/perf/build-performance" ||
|
|
48
|
+
process.env.GITHUB_EVENT_NAME !== "workflow_dispatch")
|
|
49
|
+
)
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Benchmark caches require an explicit trusted manual branch run",
|
|
52
|
+
);
|
|
53
|
+
if (
|
|
54
|
+
mode === "save" &&
|
|
55
|
+
namespace === "production" &&
|
|
56
|
+
(process.env.GITHUB_REF !== "refs/heads/main" ||
|
|
57
|
+
!["push", "workflow_dispatch"].includes(process.env.GITHUB_EVENT_NAME))
|
|
58
|
+
)
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Only trusted main builds may publish production product caches",
|
|
61
|
+
);
|
|
62
|
+
const prefix =
|
|
63
|
+
namespace === "benchmark"
|
|
64
|
+
? "wisconsin-products-benchmark-v1-"
|
|
65
|
+
: "wisconsin-products-v1-";
|
|
66
|
+
const counts = {
|
|
67
|
+
requested: 0,
|
|
68
|
+
hits: 0,
|
|
69
|
+
misses: 0,
|
|
70
|
+
restored: 0,
|
|
71
|
+
rebuild_needed: 0,
|
|
72
|
+
published: 0,
|
|
73
|
+
skipped: 0,
|
|
74
|
+
failed: 0,
|
|
75
|
+
};
|
|
76
|
+
function groupDiagnostic(group, ready, reason) {
|
|
77
|
+
console.log(
|
|
78
|
+
"Encrypted product cache group: " +
|
|
79
|
+
JSON.stringify({ mode, namespace, group: group.id, ready, reason }),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!process.env.BUILD_CACHE_KEY) {
|
|
83
|
+
console.log(
|
|
84
|
+
"Encrypted product cache unavailable; using the normal build path",
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
// Standard package resolution also supports hoisted consumer dependencies.
|
|
88
|
+
let sdk;
|
|
89
|
+
try {
|
|
90
|
+
sdk = import.meta.resolve("@actions/cache");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
93
|
+
// Legacy engine development and isolated test fixtures keep site dependencies.
|
|
94
|
+
sdk = new URL(
|
|
95
|
+
"../../../site/node_modules/@actions/cache/lib/cache.js",
|
|
96
|
+
import.meta.url,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const { restoreCache, saveCache, isFeatureAvailable } = await import(sdk);
|
|
100
|
+
if (!isFeatureAvailable())
|
|
101
|
+
throw new Error("Actions cache service is unavailable");
|
|
102
|
+
mkdirSync(path.join(site, "build/generated/product-caches"), {
|
|
103
|
+
recursive: true,
|
|
104
|
+
});
|
|
105
|
+
const products = path.join(site, "build/generated/product-caches");
|
|
106
|
+
const bundleDirectory = path.join(products, "bundles", namespace);
|
|
107
|
+
let runtime;
|
|
108
|
+
let bundleLoadPromise;
|
|
109
|
+
let bundleAvailable = false;
|
|
110
|
+
let bundleGroups = new Set();
|
|
111
|
+
const selectedBundleGroups = new Set();
|
|
112
|
+
const bundleGroupId =
|
|
113
|
+
/^(?:application|(?:global|files-global|search)-(?:public|full)|(?:course|course-files)-(?:public|full)-[a-f0-9]{16})$/;
|
|
114
|
+
function regular(file) {
|
|
115
|
+
return lstatSync(file).isFile() && realpathSync(file) === file;
|
|
116
|
+
}
|
|
117
|
+
function cleanBundleDirectory() {
|
|
118
|
+
if (realpathSync(products) !== products)
|
|
119
|
+
throw new Error("Unsafe product cache root");
|
|
120
|
+
const parent = path.dirname(bundleDirectory);
|
|
121
|
+
mkdirSync(parent, { recursive: true });
|
|
122
|
+
if (realpathSync(parent) !== parent)
|
|
123
|
+
throw new Error("Unsafe bundle cache directory");
|
|
124
|
+
if (
|
|
125
|
+
lstatSync(bundleDirectory, { throwIfNoEntry: false }) &&
|
|
126
|
+
(!lstatSync(bundleDirectory).isDirectory() ||
|
|
127
|
+
realpathSync(bundleDirectory) !== bundleDirectory)
|
|
128
|
+
)
|
|
129
|
+
throw new Error("Unsafe bundle cache directory");
|
|
130
|
+
rmSync(bundleDirectory, { recursive: true, force: true });
|
|
131
|
+
mkdirSync(bundleDirectory);
|
|
132
|
+
}
|
|
133
|
+
function bundleKey() {
|
|
134
|
+
return `${prefix}bundle-${runtime}-${process.env.GITHUB_SHA}`;
|
|
135
|
+
}
|
|
136
|
+
async function loadBundle() {
|
|
137
|
+
if (!bundleLoadPromise) bundleLoadPromise = loadBundleOnce();
|
|
138
|
+
await bundleLoadPromise;
|
|
139
|
+
}
|
|
140
|
+
async function loadBundleOnce() {
|
|
141
|
+
try {
|
|
142
|
+
cleanBundleDirectory();
|
|
143
|
+
const hit = await restoreCache([bundleDirectory], bundleKey(), [
|
|
144
|
+
`${prefix}bundle-${runtime}-`,
|
|
145
|
+
]);
|
|
146
|
+
if (!hit) return;
|
|
147
|
+
if (
|
|
148
|
+
!lstatSync(bundleDirectory).isDirectory() ||
|
|
149
|
+
realpathSync(bundleDirectory) !== bundleDirectory
|
|
150
|
+
)
|
|
151
|
+
throw new Error("Unsafe restored bundle directory");
|
|
152
|
+
const manifestFile = path.join(bundleDirectory, "bundle.json");
|
|
153
|
+
if (!regular(manifestFile) || lstatSync(manifestFile).size > 256 * 1024)
|
|
154
|
+
throw new Error("Invalid bundle metadata");
|
|
155
|
+
const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
|
|
156
|
+
if (
|
|
157
|
+
manifest.schema !== 1 ||
|
|
158
|
+
manifest.namespace !== namespace ||
|
|
159
|
+
manifest.runtime !== runtime ||
|
|
160
|
+
!Array.isArray(manifest.groups) ||
|
|
161
|
+
manifest.groups.length > 4096 ||
|
|
162
|
+
!manifest.groups.every(
|
|
163
|
+
(id) => typeof id === "string" && bundleGroupId.test(id),
|
|
164
|
+
) ||
|
|
165
|
+
new Set(manifest.groups).size !== manifest.groups.length
|
|
166
|
+
)
|
|
167
|
+
throw new Error("Invalid bundle metadata");
|
|
168
|
+
bundleGroups = new Set(manifest.groups);
|
|
169
|
+
for (const name of readdirSync(bundleDirectory)) {
|
|
170
|
+
const file = path.join(bundleDirectory, name);
|
|
171
|
+
if (
|
|
172
|
+
!regular(file) ||
|
|
173
|
+
(name !== "bundle.json" &&
|
|
174
|
+
(!name.endsWith(".gpg") || !bundleGroups.has(name.slice(0, -4))))
|
|
175
|
+
)
|
|
176
|
+
throw new Error("Unexpected bundle entry");
|
|
177
|
+
}
|
|
178
|
+
bundleAvailable = true;
|
|
179
|
+
} catch {
|
|
180
|
+
counts.failed++;
|
|
181
|
+
console.log(
|
|
182
|
+
"Optional encrypted bundle restore rejected; consumers will rebuild",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function archiveHit(group, archive) {
|
|
187
|
+
if (transport === "groups")
|
|
188
|
+
return !!(await restoreCache(
|
|
189
|
+
[archive],
|
|
190
|
+
group.key,
|
|
191
|
+
group.restoreKeys ?? [],
|
|
192
|
+
));
|
|
193
|
+
await loadBundle();
|
|
194
|
+
selectedBundleGroups.add(group.id);
|
|
195
|
+
const source = path.join(bundleDirectory, `${group.id}.gpg`);
|
|
196
|
+
if (!bundleAvailable || !bundleGroups.has(group.id) || !existsSync(source))
|
|
197
|
+
return false;
|
|
198
|
+
if (!regular(source)) throw new Error("Unsafe bundle archive");
|
|
199
|
+
mkdirSync(path.dirname(archive), { recursive: true });
|
|
200
|
+
if (
|
|
201
|
+
realpathSync(path.dirname(archive)) !== path.dirname(archive) ||
|
|
202
|
+
(lstatSync(archive, { throwIfNoEntry: false }) && !regular(archive))
|
|
203
|
+
)
|
|
204
|
+
throw new Error("Unsafe product archive destination");
|
|
205
|
+
copyFileSync(source, archive);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const planFile = "build/generated/product-caches/action-plan.json";
|
|
209
|
+
function run(args) {
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
const child = spawn("bun", ["tooling/cache.ts", ...args], {
|
|
212
|
+
cwd: site,
|
|
213
|
+
env: process.env,
|
|
214
|
+
stdio: "inherit",
|
|
215
|
+
});
|
|
216
|
+
child.once("error", reject);
|
|
217
|
+
child.once("close", (code) =>
|
|
218
|
+
code === 0
|
|
219
|
+
? resolve()
|
|
220
|
+
: reject(new Error("Build product cache command failed")),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function limited(groups, operation) {
|
|
225
|
+
let next = 0;
|
|
226
|
+
await Promise.all(
|
|
227
|
+
Array.from({ length: Math.min(4, groups.length) }, async () => {
|
|
228
|
+
while (next < groups.length) await operation(groups[next++]);
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
async function plan(kind) {
|
|
233
|
+
await run([
|
|
234
|
+
"plan",
|
|
235
|
+
"--namespace",
|
|
236
|
+
namespace,
|
|
237
|
+
...(mode === "restore" ? ["--restore"] : []),
|
|
238
|
+
...(kind ? ["--kind", kind] : []),
|
|
239
|
+
"--plan-file",
|
|
240
|
+
planFile,
|
|
241
|
+
]);
|
|
242
|
+
const value = JSON.parse(readFileSync(path.join(site, planFile), "utf8"));
|
|
243
|
+
if (
|
|
244
|
+
value.schema !== 1 ||
|
|
245
|
+
value.namespace !== namespace ||
|
|
246
|
+
!Array.isArray(value.groups)
|
|
247
|
+
)
|
|
248
|
+
throw new Error("Invalid product cache plan");
|
|
249
|
+
if (transport === "bundle") {
|
|
250
|
+
if (
|
|
251
|
+
!/^[a-f0-9]{64}$/.test(value.runtime ?? "") ||
|
|
252
|
+
(runtime && runtime !== value.runtime)
|
|
253
|
+
)
|
|
254
|
+
throw new Error("Invalid bundle runtime identity");
|
|
255
|
+
runtime = value.runtime;
|
|
256
|
+
}
|
|
257
|
+
for (const group of value.groups) {
|
|
258
|
+
if (
|
|
259
|
+
typeof group.id !== "string" ||
|
|
260
|
+
!/^[a-z0-9-]+$/.test(group.id) ||
|
|
261
|
+
group.namespace !== namespace ||
|
|
262
|
+
typeof group.key !== "string" ||
|
|
263
|
+
!group.key.startsWith(prefix) ||
|
|
264
|
+
!Array.isArray(group.restoreKeys ?? []) ||
|
|
265
|
+
!(group.restoreKeys ?? []).every(
|
|
266
|
+
(key) => typeof key === "string" && key.startsWith(prefix),
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
throw new Error("Product cache plan crosses its selected namespace");
|
|
270
|
+
if (transport === "bundle" && !bundleGroupId.test(group.id))
|
|
271
|
+
throw new Error("Unsupported bundle group");
|
|
272
|
+
const expectedArchive = path.join(
|
|
273
|
+
site,
|
|
274
|
+
"build/generated/product-caches",
|
|
275
|
+
...(namespace === "benchmark" ? ["benchmark"] : []),
|
|
276
|
+
`${group.id}.gpg`,
|
|
277
|
+
);
|
|
278
|
+
if (
|
|
279
|
+
typeof group.archive !== "string" ||
|
|
280
|
+
path.resolve(site, group.archive) !== expectedArchive
|
|
281
|
+
)
|
|
282
|
+
throw new Error("Product archive crosses its selected namespace");
|
|
283
|
+
}
|
|
284
|
+
counts.requested += value.groups.length;
|
|
285
|
+
console.log(
|
|
286
|
+
"Encrypted product cache plan: " +
|
|
287
|
+
JSON.stringify({
|
|
288
|
+
mode,
|
|
289
|
+
namespace,
|
|
290
|
+
kind: kind ?? "all",
|
|
291
|
+
groups: value.groups.length,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
return value.groups;
|
|
295
|
+
}
|
|
296
|
+
async function transfer(group, operation) {
|
|
297
|
+
const transferFile = `build/generated/product-caches/${operation}-${namespace}-${group.id}.json`;
|
|
298
|
+
rmSync(path.join(site, transferFile), { force: true });
|
|
299
|
+
await run([
|
|
300
|
+
`${operation}-products`,
|
|
301
|
+
"--namespace",
|
|
302
|
+
namespace,
|
|
303
|
+
"--group",
|
|
304
|
+
group.id,
|
|
305
|
+
"--plan-file",
|
|
306
|
+
transferFile,
|
|
307
|
+
]);
|
|
308
|
+
const value = JSON.parse(
|
|
309
|
+
readFileSync(path.join(site, transferFile), "utf8"),
|
|
310
|
+
);
|
|
311
|
+
const valid =
|
|
312
|
+
value.schema === 1 &&
|
|
313
|
+
value.namespace === namespace &&
|
|
314
|
+
Array.isArray(value.results) &&
|
|
315
|
+
value.results.length === 1 &&
|
|
316
|
+
value.results[0] !== null &&
|
|
317
|
+
value.results[0].id === group.id &&
|
|
318
|
+
typeof value.results[0].ready === "boolean";
|
|
319
|
+
if (!valid) return { ready: false, reason: "invalid-result" };
|
|
320
|
+
const result = value.results[0];
|
|
321
|
+
return {
|
|
322
|
+
ready: result.ready,
|
|
323
|
+
reason: result.ready
|
|
324
|
+
? "ready"
|
|
325
|
+
: ["missing-secret", "miss", "invalid"].includes(result.reason)
|
|
326
|
+
? result.reason
|
|
327
|
+
: "not-ready",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async function restore(kind) {
|
|
331
|
+
await limited(await plan(kind), async (group) => {
|
|
332
|
+
const archive = path.resolve(site, group.archive);
|
|
333
|
+
if (
|
|
334
|
+
!archive.startsWith(
|
|
335
|
+
path.join(site, "build/generated/product-caches") + path.sep,
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
throw new Error("Product archive escapes the cache directory");
|
|
339
|
+
try {
|
|
340
|
+
const hit = await archiveHit(group, archive);
|
|
341
|
+
if (!hit) {
|
|
342
|
+
counts.misses++;
|
|
343
|
+
counts.rebuild_needed++;
|
|
344
|
+
groupDiagnostic(group, false, "archive-miss");
|
|
345
|
+
} else {
|
|
346
|
+
counts.hits++;
|
|
347
|
+
const result = await transfer(group, "restore");
|
|
348
|
+
if (result.ready) counts.restored++;
|
|
349
|
+
else {
|
|
350
|
+
counts.rebuild_needed++;
|
|
351
|
+
groupDiagnostic(group, false, result.reason);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
counts.failed++;
|
|
356
|
+
counts.rebuild_needed++;
|
|
357
|
+
groupDiagnostic(group, false, "operation-error");
|
|
358
|
+
console.log(
|
|
359
|
+
"Optional encrypted product restore failed; consumers will validate or rebuild",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (mode === "restore") {
|
|
365
|
+
// The compact global index supplies exact immutable course product keys.
|
|
366
|
+
await restore("global");
|
|
367
|
+
await restore("application");
|
|
368
|
+
await restore("search");
|
|
369
|
+
await restore("course");
|
|
370
|
+
if (
|
|
371
|
+
transport === "bundle" &&
|
|
372
|
+
bundleAvailable &&
|
|
373
|
+
[...bundleGroups].some((id) => !selectedBundleGroups.has(id))
|
|
374
|
+
) {
|
|
375
|
+
counts.failed++;
|
|
376
|
+
console.log(
|
|
377
|
+
"Optional encrypted bundle contained unselected groups; consumers will validate or rebuild",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
const groups = await plan();
|
|
382
|
+
const prepared = [];
|
|
383
|
+
if (transport === "bundle") cleanBundleDirectory();
|
|
384
|
+
await limited(groups, async (group) => {
|
|
385
|
+
try {
|
|
386
|
+
const result = await transfer(group, "save");
|
|
387
|
+
if (!result.ready) {
|
|
388
|
+
counts.skipped++;
|
|
389
|
+
groupDiagnostic(group, false, result.reason);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const archive = path.resolve(site, group.archive);
|
|
393
|
+
if (!existsSync(archive)) {
|
|
394
|
+
counts.skipped++;
|
|
395
|
+
groupDiagnostic(group, false, "archive-missing");
|
|
396
|
+
} else if (!regular(archive)) throw new Error("Unsafe product archive");
|
|
397
|
+
else if (transport === "bundle") {
|
|
398
|
+
copyFileSync(archive, path.join(bundleDirectory, `${group.id}.gpg`));
|
|
399
|
+
prepared.push(group);
|
|
400
|
+
} else if ((await saveCache([archive], group.key)) >= 0)
|
|
401
|
+
counts.published++;
|
|
402
|
+
else {
|
|
403
|
+
counts.skipped++;
|
|
404
|
+
groupDiagnostic(group, true, "cache-not-created");
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
counts.failed++;
|
|
408
|
+
groupDiagnostic(group, false, "operation-error");
|
|
409
|
+
console.log("Optional encrypted product save failed");
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
if (transport === "bundle" && prepared.length) {
|
|
413
|
+
writeFileSync(
|
|
414
|
+
path.join(bundleDirectory, "bundle.json"),
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
schema: 1,
|
|
417
|
+
namespace,
|
|
418
|
+
runtime,
|
|
419
|
+
groups: prepared.map((group) => group.id).sort(),
|
|
420
|
+
}) + "\n",
|
|
421
|
+
{ mode: 0o600 },
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
if ((await saveCache([bundleDirectory], bundleKey())) >= 0)
|
|
425
|
+
counts.published += prepared.length;
|
|
426
|
+
else {
|
|
427
|
+
counts.skipped += prepared.length;
|
|
428
|
+
for (const group of prepared)
|
|
429
|
+
groupDiagnostic(group, true, "cache-not-created");
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
counts.failed += prepared.length;
|
|
433
|
+
for (const group of prepared)
|
|
434
|
+
groupDiagnostic(group, false, "operation-error");
|
|
435
|
+
console.log("Optional encrypted bundle save failed");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
console.log(
|
|
441
|
+
"Encrypted product cache results: " +
|
|
442
|
+
JSON.stringify({ mode, namespace, transport, ...counts }),
|
|
443
|
+
);
|
|
444
|
+
if (process.env.GITHUB_OUTPUT)
|
|
445
|
+
appendFileSync(
|
|
446
|
+
process.env.GITHUB_OUTPUT,
|
|
447
|
+
Object.entries(counts)
|
|
448
|
+
.map(([name, value]) => `${name}=${value}\n`)
|
|
449
|
+
.join(""),
|
|
450
|
+
);
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James Ding
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# twiki
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An Obsidian-compatible Markdown publishing engine with wikilinks,
|
|
4
|
+
transclusions, search, file previews, and diagrams. Build a Cloudflare Worker
|
|
5
|
+
or export public pages for a static host.
|
|
6
|
+
|
|
7
|
+
Consumers keep their notes, a declarative config, and a pinned engine dependency.
|
|
8
|
+
Installed engine files remain read-only; generated state belongs to the consumer.
|
|
9
|
+
|
|
10
|
+
## Start a vault
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
bun add --dev --exact @twango/twiki
|
|
14
|
+
bunx twiki init
|
|
15
|
+
bunx twiki dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use Bun 1.4+ and Node 22.12+. Git discovery needs Git. R worksheets need an
|
|
19
|
+
installed R renderer and their declared packages; rendering does not download
|
|
20
|
+
missing packages. `check` also needs Lychee 0.24.2 and Playwright Chromium
|
|
21
|
+
(`bunx playwright install --with-deps chromium`).
|
|
22
|
+
|
|
23
|
+
`init` creates `twiki.config.json` without overwriting an existing file:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"version": 1,
|
|
28
|
+
"contentDir": "notes",
|
|
29
|
+
"discovery": "filesystem",
|
|
30
|
+
"publication": "public",
|
|
31
|
+
"site": {
|
|
32
|
+
"name": "My notes",
|
|
33
|
+
"title": "My notes",
|
|
34
|
+
"url": "https://notes.example.com",
|
|
35
|
+
"description": "Notes and reference material."
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Without config, the current directory is the vault and publication is private.
|
|
41
|
+
Hidden directories, including `.obsidian` and `.twiki`, are excluded.
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
bunx twiki dev -- --port 5173
|
|
47
|
+
bunx twiki check
|
|
48
|
+
bunx twiki build --target cloudflare
|
|
49
|
+
bunx twiki preview
|
|
50
|
+
bunx twiki deploy
|
|
51
|
+
bunx twiki build --target static
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`--project <directory>` selects a consumer root; `--config <file>` selects an
|
|
55
|
+
explicit JSON config. Dev and preview accept extra arguments after `--`.
|
|
56
|
+
|
|
57
|
+
Static export requires `publication: "public"`; a static host cannot protect
|
|
58
|
+
private files. `.twiki/output` aliases the public static tree or the complete
|
|
59
|
+
Cloudflare artifact. `deploy` uploads an existing Cloudflare build and requires
|
|
60
|
+
explicit Cloudflare configuration and credentials.
|
|
61
|
+
|
|
62
|
+
## Cloudflare
|
|
63
|
+
|
|
64
|
+
The optional `cloudflare` object accepts Worker resource settings. Your `vars`,
|
|
65
|
+
D1 bindings, R2 buckets, routes, account ID, and compatibility settings are
|
|
66
|
+
retained; Twiki supplies generated `main` and `assets` paths.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"version": 1,
|
|
71
|
+
"contentDir": "notes",
|
|
72
|
+
"publication": "public",
|
|
73
|
+
"site": { "name": "My notes", "url": "https://notes.example.com" },
|
|
74
|
+
"cloudflare": {
|
|
75
|
+
"name": "my-notes",
|
|
76
|
+
"compatibility_date": "2026-06-07",
|
|
77
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
78
|
+
"workers_dev": true,
|
|
79
|
+
"preview_urls": false,
|
|
80
|
+
"routes": []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Provide `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` through the environment,
|
|
86
|
+
never through committed configuration. GitHub authentication uses a `DB` D1
|
|
87
|
+
binding and owner/client settings in `vars`. Without `DB`, authentication is
|
|
88
|
+
disabled and private assets remain unavailable. Analytics is disabled by default;
|
|
89
|
+
configure `analytics: {"siteId": "…", "endpoint": "https://analytics.example.com/api"}`
|
|
90
|
+
to use your own endpoint.
|
|
91
|
+
|
|
92
|
+
## Git and nested courses
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"version": 1,
|
|
97
|
+
"contentDir": "content",
|
|
98
|
+
"discovery": "git",
|
|
99
|
+
"publication": "policies"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Git discovery reads recursive tracked files and declared course submodules.
|
|
104
|
+
Twiki does not edit notes, commit submodules, or change Git pointers.
|
|
105
|
+
|
|
106
|
+
## CI and caches
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
bunx twiki prepare --json
|
|
110
|
+
bunx twiki exec -- tooling/cache.ts restore
|
|
111
|
+
bunx twiki build --target cloudflare
|
|
112
|
+
bunx twiki exec -- tooling/cache.ts save
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The stable runtime is `<consumer>/.twiki/site`. Compiler, application, search, R,
|
|
116
|
+
and Wrangler caches belong there. Engine dependencies use the shipped frozen
|
|
117
|
+
lockfile in a separate consumer-owned `node_modules`; existing vault dependencies
|
|
118
|
+
cannot change the engine compiler. Repeated preparation reuses that installation.
|
|
119
|
+
|
|
120
|
+
`prepare --json` returns `projectRoot`, `workDir`, `outputDir`, and resolved
|
|
121
|
+
`TWIKI_*` environment paths. It does not serialize inherited credentials.
|
|
122
|
+
`exec` runs Bun arguments from that runtime, including cache tools, migrations,
|
|
123
|
+
and verification scripts.
|
|
124
|
+
|
|
125
|
+
The package exports `@twango/twiki/actions/product-cache` for Node action bootstraps,
|
|
126
|
+
`@twango/twiki/cli` for locating the executable, and `@twango/twiki/runtime` for Bun scripts.
|
|
127
|
+
|
|
128
|
+
## Engine development
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
bun install --frozen-lockfile
|
|
132
|
+
bun run test
|
|
133
|
+
bun run check
|
|
134
|
+
bun cli/twiki.ts dev --project examples/vault
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`site/` contains the SvelteKit engine and its focused tests. Runtime dependencies
|
|
138
|
+
are pinned exactly so consumers can compile without a sibling checkout. The
|
|
139
|
+
engine ships a matching frozen lockfile and installs its own dependencies into
|
|
140
|
+
`.twiki/site/node_modules` with lifecycle scripts disabled. Consumer dependency
|
|
141
|
+
versions do not affect the engine graph. Bun reuses its package cache; CI can
|
|
142
|
+
retain the runtime dependency directory for subsequent runs. The compatible
|
|
143
|
+
Svelte ESM printer (`esrap`) is pinned explicitly.
|
|
144
|
+
|
|
145
|
+
The stable Wrangler 4.149.0 toolchain uses its official pinned
|
|
146
|
+
`miniflare@5.20261006.1-alpha` implementation for local Workers. Twiki pins that
|
|
147
|
+
matching version rather than selecting an independent preview runtime.
|